import { initCofing } from "./config";
import EventEmitter from 'events'
class CarrotTree extends EventEmitter {
    static stance;
    nodes = [];
    edges = [];
    config = { ...initCofing };
    paper = null;
    constructor(opts_ = {}) {
        super()
        this.config = Object.assign(this.config, { ...opts_ });
        this.init(opts_);
    }
    static getInStance(options_ = {}) {
        if (!this.stance) {
            this.stance = new CarrotTree(options_);
        }
        return this.stance;
    }
    init(opts_) {
        if (!opts_.el) {
            throw new Error("The container el element cannot be empty");
        }
        this.paper = opts_.el;
    }

    add(parent_, node_) {
        node_.fatherId = parent_.id
        // 需要找到当前父节点的所有子节点
        const children = this.nodes.filter(el => el.fatherId === parent_.id)
        // 计算子节点坐标，在添加到节点列表，统一更新数据
        this.calcNodeCoordinatesTB(parent_, [...children, node_])
        this.nodes.push(node_)
        return this.nodes;
    }

    remove(node_) {
        if (!node_) return

        const findIndex = this.nodes.findIndex(el => el.id === node_.id)
        this.nodes.splice(findIndex, 1)
        return this.nodes
    }

    selectOne (node_, index_) {
        this.nodes.forEach((el, index) => {
            if (index === index_) {
                el.isActive = !el.isActive
            } else {
                el.isActive = false
            }
        })
        this.emit('selection:updated', { nodes: this.nodes, active: this.nodes[index_] })
    }

    update() {}

    layout(nodes_ = []) {
        this.edges = []
        this.nodes = [ ...nodes_ ]
        if (this.nodes.length === 0) return null;

        // 找到根节点 fatherId=''
        const root = this.nodes.find(el => !el.fatherId)
        if (!root) {
            throw new Error('The root node cannot be empty, but its fatherId attribute can be empty.')
        } else {
            const { width, height, x:svgX, left } = this.getPaperPosition()
           
            if (this.config.rankdir === 'LR') {
                root.x = width / 10 
                root.y = height / 2 - root.node.height / 2
            }

            if (this.config.rankdir === 'TB') {
                root.x = width / 2 -  root.node.width / 2
                root.y = root.node.height / 2
            }
        }

        // 遍历所有节点，根据当前节点id寻找子集
        this.nodes.forEach(el => {
            // 查找当前节点的所有子节点
            const children = this.nodes.filter(ft => ft.fatherId === el.id)
            if (children.length) {
                // 迭代每一个子节点，并设置坐标
                // LR布局算法
                if (this.config.rankdir === 'LR') {
                    this.calcNodeCoordinatesLR(el, children)
                }
                // TB布局算法
                if (this.config.rankdir === 'TB') {
                    this.calcNodeCoordinatesTB(el, children)
                }
            } else {
                el.children = []
            }
        })

        return this.nodes
    }

    // TB 布局算法
    async calcNodeCoordinatesTB (parent, children = []) {
        const padding = this.config.padding;
        const { x: hfx, y: hfy, node: { width:parentWidth, height: parentHeight } } = parent;
        this.config.childY = hfy + parentHeight / 2 + this.config.intervalY;

        // 计算所有子节点的宽度
        const childrenAreaWidth = this.calcChildsAreasWidth(children);
        
        // 孩子区域高度顶点x坐标=startX
        this.config.startX = hfx - childrenAreaWidth / 2;

        children.forEach((child, index) => {
            // 如果子节点只有两个，需要缩小间距
            const childTbX = (this.config.startX + padding) + child.node.width / 2;
            
            child.x = childTbX;
            child.y = this.config.childY;

            // 计算边坐标
            // this.calcEgdesPointsTB(parent, child);

            const curAreaWidth = this.getNodeWidth(child);
            this.config.startX += curAreaWidth;

            // 测试挪动子孙节点，挪动过程中，要相对于当前节点坐标
            // const begatsList = this.nodes.filter(el => el.fatherId === child.id)
            // if (begatsList.length) {
            //     this.calcNodeCoordinatesTB(child, begatsList)
            // }
        })

        // 计算相邻节点偏移值，防止节点重叠
        // await this.caleMoveOffsetNodeLR(parent, childrenAreaWidth, 'x', 'width')
        // 祖先节点的同级节点的水平位置调整
        this.resetBrotherPosition(parent, childrenAreaWidth)

        // this.resetBegatsPosition(parent)
    }

    /**
     * 此方法挪动当前节点的父节点同级节点，递归到根节点
     * 调整当前的父节点的同级节点的位置，如果当前节点有子节点，子节点也跟着挪动
     * 
     * @param { Object } node_ 当前的父节点, 以下为该节点需要用到的属性
     * node.father: 节点的父节点，为null时表示父节点为根节点
     * node.children:  节点的子节点列表
     * node.x：  节点的x轴坐标
     * node.y： 节点的y轴坐标
     * @param { Number } areaHeight_ 被操作节点的区域高度
     */
    resetBrotherPosition (node_, areaHeight_) {
        // 获取当前节点的同级节点
        const levelChilds = this.nodes.filter(el => el.fatherId === node_.fatherId)
        const moveX = areaHeight_ / 4;
        // 父节点挪动差值
        let distance = 0;

        // difference差值=子节点区域总高度-区域两端补白区域-父节点自身高度
        const difference = areaHeight_ - this.config.padding * 2 - node_.node['width']
        // difference大于0才执行
        if (difference > 0) {
            levelChilds.forEach(curNode => {
                // 遍历同级节点
                if (curNode.id !== node_.id) {
                    if(curNode.x < node_.x) {
                        // 向左移动brother节点
                        curNode.x -= moveX
                        distance = -moveX
                    } else {
                        // 向右移动brother节点
                        curNode.x += moveX
                        distance = moveX
                    }

                    // 如果有子节点，子节点也得跟着挪动
                    this.resetBegatsPosition(curNode, distance)
                }
            })
        }

        // 如果根节点还存在，继续递归，直到根节点结束，根节点父节点必须为空""
        if (node_.fatherId) {
            const fatherNode = this.nodes.find(el => el.id === node_.fatherId)
            this.resetBrotherPosition(fatherNode, areaHeight_)
        }
    }

    // 子孙节点的同级节点水平位置调整，挪动过程中，要相对于当前节点坐标
    resetBegatsPosition (parent_, distance_) {
        const childrens = this.nodes.filter(el => el.fatherId === parent_.id)
        if (childrens.length) {
            childrens.forEach(el => {
                el.x = el.x + distance_
                if (el.fatherId) {
                    this.resetBegatsPosition(el, distance_)
                }
            })
        }
    }

    // LR 布局算法
    async calcNodeCoordinatesLR (parent, children = []) {
        const padding = this.config.padding;
        const { x: hfx, y: hfy, node: { width:parentWidth } } = parent;
        // 所有子节点x坐标相对于父节点居中，垂直分布，所以子节点x坐标一样
        this.config.childX = hfx + parentWidth / 2 + this.config.intervalX;
        const childrenAreaHeight = this.calcChildsAreasHeight(children);
        // 孩子区域高度顶点y坐标=startY
        this.config.startY = hfy - childrenAreaHeight / 2;

        children.forEach(child => {
            const childY = (this.config.startY + padding) + child.node.height / 2;
            // 已经求得当前子节点坐标(childX, childY)，在这里作渲染操作
            // render(childX, childY);
            child.x = this.config.childX;
            child.y = childY;

            // 计算边
            // this.calcEgdesPoints(parent, child);

            const curAreaHeight = this.getNodeHeight(child); 
            this.config.startY += curAreaHeight;
        })

        // 计算相邻节点偏移值，防止节点重叠
        await this.caleMoveOffsetNodeLR(parent, childrenAreaHeight, 'y', 'height')
    }

    // LR布局算法：解决子节点覆盖问题：
    // 当前节点的子节点区域总高度是否大于当前节点高度，如果大于需要把当前节点的同级节点挪动一下，偏移值就是该节点所有子节点区域总高度的 areaHeight * 1/2 
    // 判断条件：node.height < areaHeight / 2
    // 设这个偏移值是 moveY = areaHeight / 2 - node.height - 2 * padding
    // 挪动原理：比当前节点 y 值小的同级节点往上移 y = y + (-moveY)；比当前节点 y 值大的同级节点往下移 y = y + moveY
    caleMoveOffsetNodeLR (node, areaHeight, coordinateKey, directionKey) {
        return new Promise((resolve, reject) => {
            const halfAreaHeight = areaHeight / 2
            
            // difference差值=子节点区域总高度-区域两端补白区域-父节点自身高度
            const difference = areaHeight - this.config.padding * 2 - node.node[directionKey]
            // 查找父节点的同级节点，通过父节点的父节点id查找其父节点，一直找到根节点 root 为止
            // 找父节点的父节点用节点fatherId属性往前找
            // 差值difference大于0才执行
            if (difference > 0) {
                if (node.fatherId) {
                    // 找父节点同级节点用节点fatherId找，所有同一个父节点的子节点都拥有相同的fatherId属性值
                    const commonNodes = this.nodes.filter(el => el.fatherId === node.fatherId)
                    // 查找当前node位于那个个索引，方便挪动同级节点
                    const currentIndex = commonNodes.findIndex(el => el.id === node.id)
                    for (let i = 0; i < commonNodes.length; i++) {
                        const item = commonNodes[i];
                        // 同一个节点不做操作
                        if (item.id === node.id) return
                        // 上面的节点往上移
                        if (i < currentIndex) {
                            item[coordinateKey] += - halfAreaHeight
                            // 同时需要挪动子节点
                            this.caleChildNodeByParentLR(item, - halfAreaHeight, coordinateKey)
                        } else if (i > currentIndex) {
                            // 下面的节点往下移
                            item[coordinateKey] += halfAreaHeight
                            // 同时需要挪动子节点
                            this.caleChildNodeByParentLR(item, halfAreaHeight, coordinateKey)
                        }
                    }
                }
            }
            resolve()
        })
    }

    // 根据父节点坐标挪动子所有子节点坐标
    caleChildNodeByParentLR (parentNode = null, halfAreaHeight, coordinateKey) {
        if (!parentNode) return
        const childrenNodes = this.nodes.filter(el => el.fatherId === parentNode.id)
        childrenNodes.forEach(el => el[coordinateKey] += halfAreaHeight)
    }


    // 获取当前节点区域宽度，包含两个补白padding
    getNodeWidth (node) {
        const { node: { width }} = node
        return width ? width + this.config.padding * 2 : 0;
    }

    // 获取当前节点区域高度，包含两个补白padding
    getNodeHeight (node) {
        const { node: { height }} = node
        return height ? height + this.config.padding * 2 : 0;
    }

    // TB 布局算法
    calcEgdesPointsTB (parent, child) {
        const { x: pX, y: pY, node: { width: pWidth, height: pHeight } } = parent;
        const { x: cX, y: cY, node: { width: cWidth, height: cHeight } } = child;
        // 起点坐标sx,sy
        const sx = pX + pWidth / 2, sy = pY + pHeight;
        // 终点坐标ex,ey
        const ex = cX + cWidth / 2, ey = cY;

        // 采用三次贝塞尔曲线C，需要两个控制点，连个控制点放置points数组后面
        // 起点和终点分别用作控制点的起点和终点
        // 控制点1坐标
        const c1 = {x: sx, y: sy + (pHeight / 3)};
        // 控制点2坐标
        const c2 = {x: ex, y: ey - (cHeight / 3)};

        const points = [{x: sx, y: sy},{x: ex, y: ey }, c1, c2];

        this.edges.push({points});
    }

    // 计算边并存储
    calcEgdesPoints (parent, child) {
        // svg中渲染元素坐标是从左上角开始的，所以要想连线点位于指定位置，需要重新计算起点和终点坐标
        // 起点坐标位于元素右边中心点，终点坐标位于元素左边中心点
        const { x: pX, y: pY, node: { width: pWidth, height: pHeight } } = parent;
        const { x: cX, y: cY, node: { width: cWidth, height: cHeight } } = child;
        // 起点坐标sx,sy
        const sx = pX + pWidth, sy = pY + pHeight / 2;
        // 终点坐标ex,ey
        const ex = cX, ey = cY + cHeight / 2;
        // 采用三次贝塞尔曲线C，需要两个控制点，连个控制点放置points数组后面
        // 起点和终点分别用作控制点的起点和终点
        // 控制点1坐标
        const c1 = {x: sx + (pWidth / 3), y: sy};
        // 控制点2坐标
        const c2 = {x: ex - (cWidth / 3), y: ey };

        const points = [{x: sx, y: sy},{x: ex, y: ey }, c1, c2];

        this.edges.push({points});
    }

    // 获取边坐标，需要重新计算才能获得
    getEdges () {
        if (this.edges.length === 0) return []
        return this.edges
    }

    getEdgePoints () {
        this.nodes.forEach(el => {
            // 查找当前节点的所有子节点
            const children = this.nodes.filter(ft => ft.fatherId === el.id)
            if (children.length) {
                // 迭代每一个子节点，并设置坐标
                // LR布局算法
                if (this.config.rankdir === 'LR') {
                    children.forEach(child => this.calcEgdesPoints(el, child))
                }
                // TB布局算法
                if (this.config.rankdir === 'TB') {
                    children.forEach(child => this.calcEgdesPointsTB(el, child))
                }
            }
        })
        return this.edges
    }

    // 计算所有子节点区域宽度
    calcChildsAreasWidth (children) {
        const childAraeWidth = children.reduce((pre, cur) => {
            let totalWidth = 0
            if (cur) {
                const { node: { width }} = cur
                totalWidth += width + (2 * this.config.padding)
            }
            return pre + totalWidth
        }, 0)
        return childAraeWidth
    }

    // 计算所有子节点区域高度
    calcChildsAreasHeight (children = []) {
        // 获取所有子节点区域高度=(每个子节点高度+上下两个补白高度) * 子节点的数量
        const childAraeHeight = children.reduce((pre, cur) => {
            let totalHeight = 0;
            if (cur) {
                const { node: { height } } = cur;
                totalHeight += (height + (2 * this.config.padding))
            }
            return pre + totalHeight;
        }, 0)

        return childAraeHeight;
    }

    // 获取svg容器位置信息
    getPaperPosition() {
        if (!this.paper) {
            return {};
        }
        const { x, y, width, height, top, right, bottom, left } = this.paper.getBoundingClientRect();
        return {
            x,
            y,
            width,
            height,
            top,
            right,
            bottom,
            left,
        };
    }

    setSvgViewBox (svg) {
        const bbox = svg.getBBox();
        svg.setAttribute("viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);
    }
}

export default CarrotTree;
